package org.dromara.demo.service.impl;

import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.excel.core.DropDownOptions;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.demo.domain.vo.ExportDemoVo;
import org.dromara.demo.service.IExportExcelService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：导出下拉框Excel示例
 */
@Service
@RequiredArgsConstructor
public class ExportExcelServiceImpl implements IExportExcelService {

    /**
     * 导出数据的自定义选项方法
     *
     * @param response 用于导出数据的HttpServletResponse对象
     *
     * 此方法允许在导出数据时应用一系列自定义选项，这些选项可能包括但不限于导出格式、导出内容的筛选条件等
     * 由于需要与前端进行交互并响应导出请求，因此该方法接受HttpServletResponse对象作为参数，以便设置响应头和输出导出数据
     *
     * 注意：这是一个覆盖的方法，注释中没有提及具体实现细节，因为实现细节可能在父类或接口中已经定义
     */
    @Override
    public void exportWithOptions(HttpServletResponse response) {
        // 创建表格数据，业务中一般通过数据库查询
        List<ExportDemoVo> excelDataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            // 模拟数据库中的一条数据
            ExportDemoVo everyRowData = new ExportDemoVo();
            everyRowData.setNickName("用户-" + i);
            everyRowData.setUserStatus(UserStatus.OK.getCode());
            everyRowData.setGender("1");
            everyRowData.setPhoneNumber(String.format("175%08d", i));
            everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
            everyRowData.setProvinceId(i);
            everyRowData.setCityId(i);
            everyRowData.setAreaId(i);
            excelDataList.add(everyRowData);
        }

        // 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
        // 并通过@DropDown注解指定下拉值，或者通过创建ExcelOptions来指定下拉框
        // 使用ExcelOptions时建议指定列index，防止出现下拉列解析不对齐

        // 首先从数据库中查询下拉框内的可选项
        // 这里模拟查询结果
        List<DemoCityData> provinceList = getProvinceList(),
            cityList = getCityList(provinceList),
            areaList = getAreaList(cityList);
        int provinceIndex = 5, cityIndex = 6, areaIndex = 7;

        DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
            // 省份数据列表
            provinceList,
            // 当前选中的省份索引
            provinceIndex,
            // 城市数据列表
            cityList,
            // 当前选中的城市索引
            cityIndex,
            // 获取城市数据的ID的方法引用
            DemoCityData::getId,
            // 获取城市数据的父ID的方法引用，用于关联省份数据
            DemoCityData::getPid,
            // 对每个选项的处理
            everyOptions -> DropDownOptions.createOptionValue(
                everyOptions.getName(),
                everyOptions.getId()
            )
        );

        // 根据城市和区域数据构建下拉选项的联动选择
        // 这一步的目的是将城市列表和区域列表通过DropDownOptions类进行关联，以便在用户选择城市时，自动显示对应城市的区域选项
        DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
            // 城市列表，用于填充下拉选择中的城市部分
            cityList,
            // 当前选中城市在城市列表中的索引，用于预选中某个城市
            cityIndex,
            // 区域列表，用于填充下拉选择中的区域部分，这些区域对应于城市
            areaList,
            // 当前选中区域在区域列表中的索引，用于预选中某个区域
            areaIndex,
            // Lambda表达式，用于获取每个城市或区域的ID，这是建立关联的关键字段
            DemoCityData::getId,
            // Lambda表达式，用于获取每个城市或区域的父ID，用于确定城市和区域的层级关系
            DemoCityData::getPid,
            // 对每个选项进行处理，将其转换为下拉选择所需的值格式
            everyOptions -> DropDownOptions.createOptionValue(
                everyOptions.getName(),
                everyOptions.getId()
            )
        );

        // 把所有的下拉框存储
        List<DropDownOptions> options = new ArrayList<>();
        options.add(provinceToCity);
        options.add(cityToArea);

        // 到此为止所有的下拉框可选项已全部配置完毕

        // 接下来需要将Excel中的展示数据转换为对应的下拉选
        List<ExportDemoVo> outList = StreamUtils.toList(excelDataList, everyRowData -> {
            // 只需要处理没有使用@ExcelDictFormat注解的下拉框
            // 一般来说，可以直接在数据库查询即查询出省市县信息，这里通过模拟操作赋值
            everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
            everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
            everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
            return everyRowData;
        });

        // 导出下拉框示例数据到Excel文件
        // 此处调用ExcelUtil的exportExcel方法，实现将特定数据(outList)按照指定的导出类(ExportDemoVo.class)的格式，
        // 以及给定的文件名("下拉框示例")和ServletResponse对象(response)进行导出。
        // options参数用于指定导出过程中的额外选项或配置。
        ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
    }

    /**
     * 根据城市数据列表和ID构建选项字符串
     * 此方法主要用于从给定的城市数据列表中筛选出特定ID对应的数据，并生成一个选项字符串，用于下拉菜单等UI组件
     *
     * @param cityDataList 城市数据列表，包含多个DemoCityData对象
     * @param id 需要筛选的特定ID
     * @return 返回一个字符串，格式为"displayName:value"，其中displayName和value分别来自匹配ID的第一个DemoCityData对象如果没有匹配的ID，返回空字符串
     */
    private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
        // 将城市数据列表按ID分组，以便快速查找给定ID对应的数据
        Map<Integer, List<DemoCityData>> groupByIdMap =
            cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));

        // 检查给定的ID是否存在于分组地图中
        if (groupByIdMap.containsKey(id)) {
            // 获取与ID匹配的第一个DemoCityData对象
            DemoCityData demoCityData = groupByIdMap.get(id).get(0);
            // 使用数据构建并返回选项字符串
            return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
        } else {
            // 如果没有找到匹配的ID，返回空字符串
            return StrUtil.EMPTY;
        }
    }

    /**
     * 模拟查询数据库操作
     *
     * @return 返回一个包含省份信息的列表
     */
    private List<DemoCityData> getProvinceList() {
        List<DemoCityData> provinceList = new ArrayList<>();

        // 实际业务中一般采用数据库读取的形式，这里直接拼接创建
        provinceList.add(new DemoCityData(0, null, "安徽省"));
        provinceList.add(new DemoCityData(1, null, "江苏省"));

        return provinceList;
    }

    /**
     * 模拟查找数据库操作，需要连带查询出省的数据
     * 获取城市数据列表
     * 在实际应用中，城市数据通常从数据库中读取，但为了简化示例，这里直接创建并返回一个城市列表
     *
     * @param provinceList 模拟的父省数据 省份数据列表，用于后续省份数据的选择
     * @return 返回一个包含城市数据的列表
     */
    private List<DemoCityData> getCityList(List<DemoCityData> provinceList) {
        List<DemoCityData> cityList = new ArrayList<>();

        // 实际业务中一般采用数据库读取的形式，这里直接拼接创建
        cityList.add(new DemoCityData(0, 0, "合肥市"));
        cityList.add(new DemoCityData(1, 0, "芜湖市"));
        cityList.add(new DemoCityData(2, 1, "南京市"));
        cityList.add(new DemoCityData(3, 1, "无锡市"));
        cityList.add(new DemoCityData(4, 1, "徐州市"));

        // 调用selectParentData方法，为provinceList参数选择对应的省份数据
        // 这里暗示了存在对省份数据的依赖关系或数据处理逻辑
        selectParentData(provinceList, cityList);

        return cityList;
    }

    /**
     * 模拟查找数据库操作，需要连带查询出市的数据
     * 获取区域列表方法
     * 该方法用于初始化并返回一个包含DemoCityData对象的列表，这些对象代表不同的区域信息
     * 此处直接创建并添加DemoCityData对象至列表中，模拟从数据库读取区域数据的过程
     *
     * @param cityList 模拟的父市数据 城市列表，用于后续在区域列表中选择并设置父级数据
     * @return 返回包含区域信息的DemoCityData对象列表
     */
    private List<DemoCityData> getAreaList(List<DemoCityData> cityList) {
        List<DemoCityData> areaList = new ArrayList<>();

        // 实际业务中一般采用数据库读取的形式，这里直接拼接创建
        areaList.add(new DemoCityData(0, 0, "瑶海区"));
        areaList.add(new DemoCityData(1, 0, "庐江区"));
        areaList.add(new DemoCityData(2, 1, "南宁县"));
        areaList.add(new DemoCityData(3, 1, "镜湖区"));
        areaList.add(new DemoCityData(4, 2, "玄武区"));
        areaList.add(new DemoCityData(5, 2, "秦淮区"));
        areaList.add(new DemoCityData(6, 3, "宜兴市"));
        areaList.add(new DemoCityData(7, 3, "新吴区"));
        areaList.add(new DemoCityData(8, 4, "鼓楼区"));
        areaList.add(new DemoCityData(9, 4, "丰县"));

        // 调用方法根据城市列表选择并设置区域列表的父级数据
        selectParentData(cityList, areaList);

        return areaList;
    }

    /**
     * 模拟数据库的查询父数据操作
     * 为子列表中的每个城市数据对象选择对应的父列表数据
     * 主要功能是根据父ID匹配父数据对象，并将其设置到子数据对象中
     *
     * @param parentList 父列表数据，包含多个DemoCityData对象
     * @param sonList 子列表数据，每个DemoCityData对象可能有对应的父对象
     */
    private void selectParentData(List<DemoCityData> parentList, List<DemoCityData> sonList) {
        // 将父列表按照ID进行分组，便于后续查找
        Map<Integer, List<DemoCityData>> parentGroupByIdMap =
            parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
        // 遍历子列表，为每个子数据对象设置父数据对象
        sonList.forEach(everySon -> {
            // 如果当前子数据的父ID存在于父数据列表中
            if (parentGroupByIdMap.containsKey(everySon.getPid())) {
                // 设置当前子数据的父数据
                everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
            }
        });
    }

    /**
     * 模拟的数据库省市县
     */
    @Data
    private static class DemoCityData {
        /**
         * 数据库id字段
         */
        private Integer id;
        /**
         * 数据库pid字段
         */
        private Integer pid;
        /**
         * 数据库name字段
         */
        private String name;
        /**
         * MyBatisPlus连带查询父数据
         */
        private DemoCityData pData;

        public DemoCityData(Integer id, Integer pid, String name) {
            this.id = id;
            this.pid = pid;
            this.name = name;
        }
    }
}
